package controller

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"slices"
	"strconv"
	"time"

	"github.com/bytedance/sonic"
	"github.com/gin-gonic/gin"
	"github.com/labring/aiproxy/core/common/reqlimit"
	"github.com/labring/aiproxy/core/controller/utils"
	"github.com/labring/aiproxy/core/middleware"
	"github.com/labring/aiproxy/core/model"
	"github.com/labring/aiproxy/core/relay/mode"
	"gorm.io/gorm"
)

func getDashboardTime(
	t, timespan string,
	startTime, endTime time.Time,
	timezoneLocation *time.Location,
) (time.Time, time.Time, model.TimeSpanType) {
	end := time.Now()
	if !endTime.IsZero() {
		end = endTime
	}

	if timezoneLocation == nil {
		timezoneLocation = time.Local
	}

	var (
		start    time.Time
		timeSpan model.TimeSpanType
	)

	switch t {
	case "month":
		start = end.AddDate(0, 0, -30)
		start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
		timeSpan = model.TimeSpanDay
	case "two_week":
		start = end.AddDate(0, 0, -15)
		start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
		timeSpan = model.TimeSpanDay
	case "week":
		start = end.AddDate(0, 0, -7)
		start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
		timeSpan = model.TimeSpanDay
	case "day":
		start = end.AddDate(0, 0, -1)
		timeSpan = model.TimeSpanHour
	default:
		start = end.AddDate(0, 0, -7)
		timeSpan = model.TimeSpanHour
	}

	if !startTime.IsZero() {
		start = startTime
	}

	switch model.TimeSpanType(timespan) {
	case model.TimeSpanDay, model.TimeSpanHour, model.TimeSpanMonth:
		timeSpan = model.TimeSpanType(timespan)
	}

	return start, end, timeSpan
}

func fillGaps(
	data []model.ChartData,
	start, end time.Time,
	t model.TimeSpanType,
) []model.ChartData {
	if len(data) == 0 || t == model.TimeSpanMonth {
		return data
	}

	var timeSpan time.Duration
	switch t {
	case model.TimeSpanDay:
		timeSpan = time.Hour * 24
	case model.TimeSpanHour:
		timeSpan = time.Hour
	case model.TimeSpanMinute:
		timeSpan = time.Minute
	default:
		return data
	}

	// Handle first point
	firstPoint := time.Unix(data[0].Timestamp, 0)

	firstAlignedTime := firstPoint
	for !firstAlignedTime.Add(-timeSpan).Before(start) {
		firstAlignedTime = firstAlignedTime.Add(-timeSpan)
	}

	var firstIsZero bool
	if !firstAlignedTime.Equal(firstPoint) {
		data = append([]model.ChartData{
			{
				Timestamp: firstAlignedTime.Unix(),
			},
		}, data...)
		firstIsZero = true
	}

	// Handle last point
	lastPoint := time.Unix(data[len(data)-1].Timestamp, 0)

	lastAlignedTime := lastPoint
	for !lastAlignedTime.Add(timeSpan).After(end) {
		lastAlignedTime = lastAlignedTime.Add(timeSpan)
	}

	var lastIsZero bool
	if !lastAlignedTime.Equal(lastPoint) {
		data = append(data, model.ChartData{
			Timestamp: lastAlignedTime.Unix(),
		})
		lastIsZero = true
	}

	result := make([]model.ChartData, 0, len(data))
	result = append(result, data[0])

	for i := 1; i < len(data); i++ {
		curr := data[i]
		prev := data[i-1]
		hourDiff := (curr.Timestamp - prev.Timestamp) / int64(timeSpan.Seconds())

		// If gap is 1 hour or less, continue
		if hourDiff <= 1 {
			result = append(result, curr)
			continue
		}

		// If gap is more than 3 hours, only add boundary points
		if hourDiff > 3 {
			// Add point for hour after prev
			if i != 1 || (i == 1 && !firstIsZero) {
				result = append(result, model.ChartData{
					Timestamp: prev.Timestamp + int64(timeSpan.Seconds()),
				})
			}
			// Add point for hour before curr
			if i != len(data)-1 || (i == len(data)-1 && !lastIsZero) {
				result = append(result, model.ChartData{
					Timestamp: curr.Timestamp - int64(timeSpan.Seconds()),
				})
			}

			result = append(result, curr)

			continue
		}

		// Fill gaps of 2-3 hours with zero points
		for j := prev.Timestamp + int64(timeSpan.Seconds()); j < curr.Timestamp; j += int64(timeSpan.Seconds()) {
			result = append(result, model.ChartData{
				Timestamp: j,
			})
		}

		result = append(result, curr)
	}

	return result
}

// GetDashboard godoc
//
//	@Summary		Get dashboard data
//	@Description	Returns the general dashboard data including usage statistics and metrics
//	@Tags			dashboard
//	@Produce		json
//	@Security		ApiKeyAuth
//	@Param			channel			query		int		false	"Channel ID"
//	@Param			model			query		string	false	"Model name"
//	@Param			start_timestamp	query		int64	false	"Start second timestamp"
//	@Param			end_timestamp	query		int64	false	"End second timestamp"
//	@Param			timezone		query		string	false	"Timezone, default is Local"
//	@Param			timespan		query		string	false	"Time span type (minute, hour, day, month)"
//	@Success		200				{object}	middleware.APIResponse{data=model.DashboardResponse}
//	@Router			/api/dashboard/ [get]
func GetDashboard(c *gin.Context) {
	startTime, endTime := utils.ParseTimeRange(c, -1)
	timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
	timespan := c.Query("timespan")
	start, end, timeSpan := getDashboardTime(
		c.Query("type"),
		timespan,
		startTime,
		endTime,
		timezoneLocation,
	)
	modelName := c.Query("model")
	channelStr := c.Query("channel")
	channelID, _ := strconv.Atoi(channelStr)

	dashboards, err := model.GetDashboardData(
		start,
		end,
		modelName,
		channelID,
		timeSpan,
		timezoneLocation,
	)
	if err != nil {
		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	dashboards.ChartData = fillGaps(dashboards.ChartData, start, end, timeSpan)

	if channelID == 0 {
		channelStr = "*"
	}

	rpm, _ := reqlimit.GetChannelModelRequest(c.Request.Context(), channelStr, modelName)
	dashboards.RPM = rpm
	tpm, _ := reqlimit.GetChannelModelTokensRequest(c.Request.Context(), channelStr, modelName)
	dashboards.TPM = tpm

	middleware.SuccessResponse(c, dashboards)
}

// GetGroupDashboard godoc
//
//	@Summary		Get dashboard data for a specific group
//	@Description	Returns dashboard data and metrics specific to the given group
//	@Tags			dashboard
//	@Produce		json
//	@Security		ApiKeyAuth
//	@Param			group			path		string	true	"Group"
//	@Param			token_name		query		string	false	"Token name"
//	@Param			model			query		string	false	"Model or *"
//	@Param			start_timestamp	query		int64	false	"Start second timestamp"
//	@Param			end_timestamp	query		int64	false	"End second timestamp"
//	@Param			timezone		query		string	false	"Timezone, default is Local"
//	@Param			timespan		query		string	false	"Time span type (minute, hour, day, month)"
//	@Success		200				{object}	middleware.APIResponse{data=model.GroupDashboardResponse}
//	@Router			/api/dashboard/{group} [get]
func GetGroupDashboard(c *gin.Context) {
	group := c.Param("group")
	if group == "" {
		middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
		return
	}

	startTime, endTime := utils.ParseTimeRange(c, -1)
	timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
	timespan := c.Query("timespan")
	start, end, timeSpan := getDashboardTime(
		c.Query("type"),
		timespan,
		startTime,
		endTime,
		timezoneLocation,
	)
	tokenName := c.Query("token_name")
	modelName := c.Query("model")

	dashboards, err := model.GetGroupDashboardData(
		group,
		start,
		end,
		tokenName,
		modelName,
		timeSpan,
		timezoneLocation,
	)
	if err != nil {
		middleware.ErrorResponse(c, http.StatusInternalServerError, "failed to get statistics")
		return
	}

	dashboards.ChartData = fillGaps(dashboards.ChartData, start, end, timeSpan)

	rpm, _ := reqlimit.GetGroupModelTokennameRequest(
		c.Request.Context(),
		group,
		modelName,
		tokenName,
	)
	dashboards.RPM = rpm
	tpm, _ := reqlimit.GetGroupModelTokennameTokensRequest(
		c.Request.Context(),
		group,
		modelName,
		tokenName,
	)
	dashboards.TPM = tpm

	middleware.SuccessResponse(c, dashboards)
}

type GroupModel struct {
	CreatedAt int64                        `json:"created_at,omitempty"`
	UpdatedAt int64                        `json:"updated_at,omitempty"`
	Config    map[model.ModelConfigKey]any `json:"config,omitempty"`
	Model     string                       `json:"model"`
	Owner     model.ModelOwner             `json:"owner"`
	Type      mode.Mode                    `json:"type"`
	RPM       int64                        `json:"rpm,omitempty"`
	TPM       int64                        `json:"tpm,omitempty"`
	// map[size]map[quality]price_per_image
	ImageQualityPrices map[string]map[string]float64 `json:"image_quality_prices,omitempty"`
	// map[size]price_per_image
	ImagePrices    map[string]float64 `json:"image_prices,omitempty"`
	Price          model.Price        `json:"price,omitempty"`
	EnabledPlugins []string           `json:"enabled_plugins,omitempty"`
}

func getEnabledPlugins(plugin map[string]json.RawMessage) []string {
	enabledPlugins := make([]string, 0, len(plugin))
	for pluginName, pluginConfig := range plugin {
		pluginConfigNode, err := sonic.Get(pluginConfig)
		if err != nil {
			continue
		}

		if enable, err := pluginConfigNode.Get("enable").Bool(); err == nil && enable {
			enabledPlugins = append(enabledPlugins, pluginName)
		}
	}

	return enabledPlugins
}

func NewGroupModel(mc model.ModelConfig) GroupModel {
	gm := GroupModel{
		Config:             mc.Config,
		Model:              mc.Model,
		Owner:              mc.Owner,
		Type:               mc.Type,
		RPM:                mc.RPM,
		TPM:                mc.TPM,
		ImageQualityPrices: mc.ImageQualityPrices,
		ImagePrices:        mc.ImagePrices,
		Price:              mc.Price,
		EnabledPlugins:     getEnabledPlugins(mc.Plugin),
	}
	if !mc.CreatedAt.IsZero() {
		gm.CreatedAt = mc.CreatedAt.Unix()
	}

	if !mc.UpdatedAt.IsZero() {
		gm.UpdatedAt = mc.UpdatedAt.Unix()
	}

	return gm
}

// GetGroupDashboardModels godoc
//
//	@Summary		Get model usage data for a specific group
//	@Description	Returns model-specific metrics and usage data for the given group
//	@Tags			dashboard
//	@Produce		json
//	@Security		ApiKeyAuth
//	@Param			group	path		string	true	"Group"
//	@Success		200		{object}	middleware.APIResponse{data=[]GroupModel}
//	@Router			/api/dashboard/{group}/models [get]
func GetGroupDashboardModels(c *gin.Context) {
	group := c.Param("group")
	if group == "" {
		middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
		return
	}

	groupCache, err := model.CacheGetGroup(group)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			middleware.SuccessResponse(
				c,
				model.LoadModelCaches().EnabledModelConfigsBySet[model.ChannelDefaultSet],
			)
		} else {
			middleware.ErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("failed to get group: %v", err))
		}

		return
	}

	availableSet := groupCache.GetAvailableSets()
	enabledModelConfigs := model.LoadModelCaches().EnabledModelConfigsBySet

	newEnabledModelConfigs := make([]GroupModel, 0)
	for _, set := range availableSet {
		for _, mc := range enabledModelConfigs[set] {
			if slices.ContainsFunc(newEnabledModelConfigs, func(m GroupModel) bool {
				return m.Model == mc.Model
			}) {
				continue
			}

			newEnabledModelConfigs = append(
				newEnabledModelConfigs,
				NewGroupModel(
					middleware.GetGroupAdjustedModelConfig(*groupCache, mc),
				),
			)
		}
	}

	middleware.SuccessResponse(c, newEnabledModelConfigs)
}

// GetTimeSeriesModelData godoc
//
//	@Summary		Get model usage data for a specific channel
//	@Description	Returns model-specific metrics and usage data for the given channel
//	@Tags			dashboard
//	@Produce		json
//	@Security		ApiKeyAuth
//	@Param			channel			query		int		false	"Channel ID"
//	@Param			model			query		string	false	"Model name"
//	@Param			start_timestamp	query		int64	false	"Start timestamp"
//	@Param			end_timestamp	query		int64	false	"End timestamp"
//	@Param			timezone		query		string	false	"Timezone, default is Local"
//	@Param			timespan		query		string	false	"Time span type (minute, hour, day, month)"
//	@Success		200				{object}	middleware.APIResponse{data=[]model.TimeSummaryDataV2}
//	@Router			/api/dashboardv2/ [get]
func GetTimeSeriesModelData(c *gin.Context) {
	channelID, _ := strconv.Atoi(c.Query("channel"))
	modelName := c.Query("model")
	startTime, endTime := utils.ParseTimeRange(c, -1)
	timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))

	models, err := model.GetTimeSeriesModelData(
		channelID,
		modelName,
		startTime,
		endTime,
		model.TimeSpanType(c.Query("timespan")),
		timezoneLocation,
	)
	if err != nil {
		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	middleware.SuccessResponse(c, models)
}

// GetGroupTimeSeriesModelData godoc
//
//	@Summary		Get model usage data for a specific group
//	@Description	Returns model-specific metrics and usage data for the given group
//	@Tags			dashboard
//	@Produce		json
//	@Security		ApiKeyAuth
//	@Param			group			path		string	true	"Group"
//	@Param			token_name		query		string	false	"Token name"
//	@Param			model			query		string	false	"Model name"
//	@Param			start_timestamp	query		int64	false	"Start timestamp"
//	@Param			end_timestamp	query		int64	false	"End timestamp"
//	@Param			timezone		query		string	false	"Timezone, default is Local"
//	@Param			timespan		query		string	false	"Time span type (minute, hour, day, month)"
//	@Success		200				{object}	middleware.APIResponse{data=[]model.TimeSummaryDataV2}
//	@Router			/api/dashboardv2/{group} [get]
func GetGroupTimeSeriesModelData(c *gin.Context) {
	group := c.Param("group")
	if group == "" {
		middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
		return
	}

	tokenName := c.Query("token_name")
	modelName := c.Query("model")
	startTime, endTime := utils.ParseTimeRange(c, -1)
	timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))

	models, err := model.GetGroupTimeSeriesModelData(
		group,
		tokenName,
		modelName,
		startTime,
		endTime,
		model.TimeSpanType(c.Query("timespan")),
		timezoneLocation,
	)
	if err != nil {
		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	middleware.SuccessResponse(c, models)
}
